package org.egrit;

import java.awt.Point;
import java.awt.Rectangle;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.Polygon;

/**
 * Provides static methods to create grid bounds objects. These should be used
 * in preference to the constructors of the {@linkplain IGridBounds} classes
 * since they return optimal classes (e.g. {@linkplain EmptyGridBounds} for 
 * empty bounds parameters).
 * <p>
 * Implementation note
 * <br>
 * The various bounds classes are hard-coded into this factory class rather 
 * than, for example, being loaded from a registry and selected according to
 * requirements.
 * 
 * @author michael
 */
public class GridBoundsFactory {
    
    private GridBoundsFactory() {}
    
    /**
     * Creates an empty bounds instance.
     * 
     * @return new bounds instance
     */
    public static IGridBounds create() {
        return new EmptyGridBounds();
    }
    
    /**
     * Creates a bounds instance for a single grid cell.
     * 
     * @param gridX grid X ordinate
     * @param gridY grid Y ordinate
     * 
     * @return new bounds instance
     */
    public static IGridBounds create(int gridX, int gridY) {
        return new CellGridBounds(gridX, gridY);
    }
    
    /**
     * Creates a bounds instance for a single grid cell.
     * 
     * @param gridPos grid cell position
     * 
     * @return new bounds instance
     */
    public static IGridBounds create(Point gridPos) {
        if (gridPos == null) {
            return new EmptyGridBounds();
        }
        
        return new CellGridBounds(gridPos.x, gridPos.y);
    }
    
    /**
     * Creates a new bounds instance.
     * 
     * @param minGridX minimum grid X ordinate of included cell
     * @param maxGridX maximum grid X ordinate of included cell
     * @param minGridY minimum grid Y ordinate of included cell
     * @param maxGridY maximum grid Y ordinate of included cell
     * 
     * @return new bounds instance
     */
    public static IGridBounds create(int minGridX, int maxGridX, int minGridY, int maxGridY) {
        final int dx = maxGridX - minGridX;
        final int dy = maxGridY - minGridY;
        
        if (dx < 0 || dy < 0) {
            return new EmptyGridBounds();
        }
        
        if (dx == 0 && dy == 0) {
            return new CellGridBounds(minGridX, minGridY);
            
        }
        
        return new RectGridBounds(minGridX, maxGridX, minGridY, maxGridY);
    }
    
    /**
     * Creates an new bounds instance for cells whose centres lie within the given envelope.
     * 
     * @param env an envelope with coordinates in fractional grid units 
     * 
     * @return new bounds instance
     */
    public static IGridBounds create(Envelope env) {
        if (env == null || env.isNull()) {
            return new EmptyGridBounds();
            
        } else {
            int minGridX = (int) Math.round(env.getMinX());
            int minGridY = (int) Math.round(env.getMinY());
            int maxGridX = (int) Math.round(env.getMaxX()) - 1;
            int maxGridY = (int) Math.round(env.getMaxY()) - 1;

            return create(minGridX, maxGridX, minGridY, maxGridY);
        }
    }
    
    /**
     * Creates a new bounds instance based on the envelope of the given geometry.
     * 
     * @param geom the geometry
     * 
     * @return new bounds instance
     */
    public static IGridBounds create(Geometry geom) {
        if (geom == null) {
            return create();
        }
        
        if (geom.getNumPoints() == 1) {
            // geometry is a single point - interpret its coordinate
            // as fractional grid cells
            Coordinate c = geom.getCoordinate();
            return create((int) c.x, (int) c.y);
        }
        
        return create(geom.getEnvelopeInternal());
    }

    /**
     * Creates a new bounds instance. The input rectangle is treated as the
     * <strong>outer</strong> limits of a grid region, as opposed to the included cell limits. 
     * So, for example, the {@code Rectangle(x=0, y=0, width=10, height=20)} 
     * includes the cell at {@code (x=9, y=19)} but not {@code (x=10, y=20)}.
     * 
     * @param rect rectangle defining the outer limits of the grid region
     * 
     * @return new bounds instance
     */
    public static IGridBounds create(Rectangle rect) {
        if (rect == null || rect.isEmpty()) {
            return new EmptyGridBounds();
        }
        
        if (rect.width == 1 && rect.height == 1) {
            return create(rect.x, rect.y);
        }
        
        return create(rect.x, rect.x + rect.width - 1, rect.y, rect.y + rect.height - 1);
    }
    
    /**
     * Creates a new bound instance based on an existing instance. Note that 
     * the new instance will not necessarily be the same class as the 
     * existing instance.
     * 
     * @param bounds the existing bounds
     * 
     * @return new bounds instance
     */
    public static IGridBounds create(IGridBounds bounds) {
        if (bounds == null || bounds.isEmpty()) {
            return new EmptyGridBounds();
        }
        
        if (bounds.getArea() == 1) {
            return new CellGridBounds(bounds.getMinCellPos());
        }
        
        return new RectGridBounds(
                bounds.getMinGridX(), bounds.getMaxGridX(), 
                bounds.getMinGridY(), bounds.getMaxGridY());
    }

    /**
     * Creates a new bounds instance by expanding the input bounds by
     * the given distance.
     * 
     * @param distance distance to expand (grid cells; may be negative)
     * 
     * @return new bounds instance
     */
    public static IGridBounds createExpanded(IGridBounds bounds, int distance) {
        if (distance == 0) {
            return create(bounds);
        }
        
        int newWidth = bounds.getWidth() + 2 * distance;
        int newHeight = bounds.getHeight() + 2 * distance;
        
        if (newWidth < 1 || newHeight < 1) {
            // result is empty
            return create();
        }
        
        return create(
                bounds.getMinGridX() - distance, 
                bounds.getMaxGridX() + distance,
                bounds.getMinGridY() - distance,
                bounds.getMaxGridY() + distance);
    }


}
